从零开始 node(二):Event Loop
朱耀华
第三、四章讲的是异步 I/O、异步编程,而实现异步的工具就是 Event Loop。在这里可以将浏览器环境和 Node 环境下的 Event Loop 对比来学习。
首先看一个栗子:
1 | setTimeout(() => { |
产生原因就在于两种环境下的 Event Loop 策略和过程不同。
浏览器环境下的 Event Loop
浏览器环境下的异步操作分为两种:宏任务(macrotask)和微任务(microtask)。
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
微任务包括 process.nextTick ,promise ,Object.observe ,MutationObserver
重点来了:
在每一次 Event Loop 中,从宏任务队列中取出一个任务执行,该任务执行的过程中微任务的回调函数加入到微任务队列中,当宏任务执行结束,开始执行微任务,直到队列清空。再取出下一个宏任务重复以上操作。
举个栗子:
1 | setTimeout(function() { |
看懂这个你才算真正理解了浏览器端的 Event Loop。
流程分析:
- 首先从宏任务队列中取出一个任务即 script,顺序执行。
- setTimeOut 立即执行(注意宏任务微任务区分的是回调),将回调函数加入宏任务队尾,继续执行。
- new Promise 立即执行构造函数,输出 2,3,在循环 1000 次之后执行了 resolve,将 then 的回调函数加入微任务队尾。
- 输出 5,该宏任务执行完成,开始执行微任务队列,取出队首 then 的回调函数执行,输出 4。
- 微任务队列清空,取出宏任务队首的 setTimeOut 回调,输出 1。
这时我们来重新分析一下开始的栗子:
- setTimeOut1,2 分别加入宏任务队列。
- script 执行结束,微任务队列为空,取出宏任务队列 setTimeOut1 的回调,输出 timer1,promise 加入微任务队列。
- 取出微任务队列队首,输出 promise1。
- 取出 setTimeOut2 的回调,输出 timer2,promise 加入微任务队列。
- 取出微任务队列队首,输出 promise2。
至此,浏览器端的 Event Loop 分析完成
Node 环境下的 Event Loop
分析 Node.js libuv 库源码:
1 | int uv_run(uv_loop_t* loop, uv_run_mode mode) { |
1 | ┌───────────────────────┐ |
- timers 阶段:执行 timer(setTimeout、setInterval)的回调
- I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
- idle, prepare 阶段:仅 node 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
- check 阶段:执行 setImmediate() 的回调
- close callbacks 阶段:执行 socket 的 close 事件回调
可以这样理解:
在 node 中每一个阶段都维护一个宏任务和微任务队列。在宏任务执行结束之后,执行一次微任务队列中的任务。在等待 I/O 的时候,node 线程会阻塞在 poll 阶段。
timers 阶段
timers 阶段 Node 会检查 timer 是否已过期,如果有就把它的回调压入任务队列中执行。但是 js 的定时器并不靠谱,因为在定时器过期的时候,js 的线程可能会在执行另外一个任务。
setTimeOut 和 setImmediate 的执行顺序也是不固定的。如下:
1 | setTimeout(() => { |
看起来任务是从 timers 阶段开始的,但是如果我们从 poll 阶段的 I/O 开始执行这段代码,就是 setImmediate 先执行,因为 poll 阶段之后就是 check 阶段,执行 setImmediate 代码。
poll 阶段
当有已超时的 timer,执行它的回调函数。同步执行 poll 队列里的回调,直到队列为空或执行的回调达到系统上限,而后检查有无预设的 setImmediate(),分两种情况:
- 有预设的 setImmediate(),poll 阶段进入 check 阶段,并执行 check 阶段的任务队列;
- 无预设的 setImmediate(),阻塞在该阶段等待。如果 timer 队列非空,则开始下一轮事件循环,重新进入到 timer 阶段。
check 阶段
setImmediate()的回调加入到 check 队列中顺序执行。
这时候文章开始的例子就比较容易理解了。我们对照着来看其他几个类似案例:
1 | // 加入两个setImmediate()的回调函数 |
两个 setImmediate 先执行,且nextTick
队列优先清空,然后再执行其他队列。
至此,js 执行的两个环境的异步 Event Loop 执行顺序已经介绍清楚。大家可以自己动手来试试执行一下。